
//Copyright 1997-2009 Syrinx Development, Inc.
//This file is part of the Syrinx Web Application Framework (SWAF).
// == BEGIN LICENSE ==
//
// Licensed under the terms of any of the following licenses at your
// choice:
//
//  - GNU General Public License Version 3 or later (the "GPL")
//    http://www.gnu.org/licenses/gpl.html
//
//  - GNU Lesser General Public License Version 3 or later (the "LGPL")
//    http://www.gnu.org/licenses/lgpl.html
//
//  - Mozilla Public License Version 1.1 or later (the "MPL")
//    http://www.mozilla.org/MPL/MPL-1.1.html
//
// == END LICENSE ==
using System;
using System.Collections;
using System.Diagnostics;
using System.IO;
using System.Text.RegularExpressions;
using System.Xml;


using Swaf.Versioning;
using Swaf;

namespace Swaf.Container
{
	/// <summary>
	/// This class inherits from the XmlCatalogContainerNode and provides a versioning implementation of an XML Catalog.
	/// </summary>
	public class VersioningXmlCatalogContainerNode : XmlCatalogContainerNode
	{
		protected BaseVersioningPartner m_versioning;
		protected string m_versioningDelimiter;

		/// <summary>
		/// Constructor adds creating a reference to the versioning partner in order to resolve names to versioned names based on context information.
		/// </summary>
		/// <param name="mr"></param>
		/// <param name="extraInfo"></param>
		public VersioningXmlCatalogContainerNode(Match mr, object[] extraInfo)
			: base(mr, extraInfo)
		{
			IApplication app = Application.currentApp;
			m_versioning = (BaseVersioningPartner)app.appData[@"sysObjs-Versioning"];
			m_versioningDelimiter = BaseVersioningPartner.VERSION_DELIMITER;

		}
		#region INameAddressSupport Members

		/// <summary>
		/// Overridden Get method provides a versioned get.  If the requested key is not in the cache dictionary,
		/// the system will look for an exact match in the catalog folder.  If that match is not found, the search
		/// key is truncated by 1 delimited value and the search is performed again.  This process is repeated until
		/// either a file is found or the key becomes empty.
		/// </summary>
		/// <param name="nameAddress"></param>
		/// <param name="infoArgs"></param>
		/// <returns></returns>
		public override object get(string nameAddress, params object[] infoArgs)
		{
			object val = null;
			string cachePath = "";
			IApplication app = Application.currentApp;
			if (nameAddress == null)
			{
				return this;
			}
			IList versioningInfo = null;
			NameInfo nameInfo = calculateNameInfo(nameAddress);
			string name = nameInfo.CatalogEntryName;
			string path = Path.Combine(m_base.FullName, nameInfo.CatalogLocationName);

			//if the versioning partner is configured, use it to resolve the name.
			if (m_versioning != null)
			{
				versioningInfo = m_versioning.getVersionInfo(app, name, path, m_catalogName);
				name = m_versioning.resolve(name);
				if (versioningInfo == null)
				{
					path = m_versioning.resolvePath(path);
				}
			}

			//Check to see if an entry is already cached under the full versioned name
			if ((val = m_map.get(name)) == null)
			{
				//Support old style of work
				if (versioningInfo == null)
				{
					//If not, we want to use the resolved file name from here on out for searching the file system.
					string newName = name;

					//If the key was not found in the map, look it up in the folder.  Keep removing 1 delimited value until
					//either the key is "" or a file is found


					FileInfo f = getCatalogFileInfo(path, newName.Replace(BaseVersioningPartner.VERSION_DELIMITER, "."), ref cachePath); //new FileInfo(Path.Combine(m_base.FullName, newName.Replace("::",".") + ".xml"));

					while ((!newName.Equals("")) && (!f.Exists))
					{
						//Remove the last delimited value from newKey and continue
						int delimiterLocation = newName.LastIndexOf(m_versioningDelimiter);
						if (delimiterLocation > 0)
						{
							newName = newName.Substring(0, delimiterLocation);
							f = getCatalogFileInfo(path, newName.Replace(BaseVersioningPartner.VERSION_DELIMITER, "."), ref cachePath); //new FileInfo(Path.Combine(m_base.FullName, newName.Replace("::",".") + ".xml"));
						}
						else
						{
							//There are no more delimited values, set new key to "" so we can exit from the loop.
							newName = "";
						}
					}

					if (f.Exists)
					{
						val = getFromXmlFile(app, nameInfo, f, cachePath, infoArgs);

						// Cache the value if appropriate
						bool isCacheableEntry = true;

						ICatalogItem info = val as ICatalogItem;
						if (info != null)
						{
							info.catalogName = m_catalogName;
							info.expiration = "expire on change of " + cachePath;
							addDefaultExpire(app, info);
							info.key = nameInfo.CatalogEntryName;
							info.searchPath = cachePath;

							isCacheableEntry = info.isCacheable;
						}

						if (isCacheableEntry)
						{
							string cacheKey = nameInfo.CatalogEntryName;
							cacheEntry(cachePath, cacheKey, val);
						}
					}
					else
						throw new CatalogException(String.Format("Cannot find catalog extry '{0}' in catalog", name));
				}
				else
				{
					/* This new mechanism gets a list of VersionedCatalogSearchEntry objects
					 * from the versioning partner and iterates them to find the file.
					 * Each entry in the list contains a single path, a file name and 
					 * a versioned string tying this entry to a particular segment of 
					 * the overall versioned string.  An example is:
					 *	Versioned String=GetCustomers::Signature::0504::Coke
					 *  Entries would be:
					 *		Entry 1:
					 *			versionString=GetCustomers::Signature::0504::Coke
					 *			versionPath=..\DataRequestDefinitions\Signature\0504\Coke
					 *			searchFileName=GetCustomers
					 *		Entry 2:
					 *			versionString=GetCustomers::Signature::0504
					 *			versionPath=..\DataRequestDefinitions\Signature\0504
					 *			searchFileName=GetCustomers
					 *		Entry 3:
					 *			versionString=GetCustomers::Signature
					 *			versionPath=..\DataRequestDefinitions\Signature
					 *			searchFileName=GetCustomers
					 *		Entry 4:
					 *			versionString=GetCustomers
					 *			versionPath=..\DataRequestDefinitions
					 *			searchFileName=GetCustomers
					 */
					int idx = 0;
					Stack pendingCacheKeys = new Stack();
					foreach (VersionedCatalogSearchEntry entry in versioningInfo)
					{
						FileInfo f = getFileInfo(entry.versionPath, entry.searchFileName);
						//Build the cache path string as we go
						if (cachePath.Length == 0)
						{
							cachePath = entry.versionPath + @"\" + f.Name;
						}
						else
						{
							cachePath += "," + entry.versionPath + @"\" + f.Name;
						}

						if (f.Exists)
						{
							//Make sure that this enty has not already been cached.
							val = m_map[entry.versionString];
							if (val == null)
							{
								val = getFromXmlFile(app, nameInfo, f, cachePath, infoArgs);
								bool isCacheableEntry = true;

								ICatalogItem info = val as ICatalogItem;
								if (info != null)
								{
									info.catalogName = m_catalogName;
									info.expiration = "expire on change of " + cachePath;
									addDefaultExpire(app, info);
									info.key = entry.versionString;
									info.searchPath = cachePath;

									isCacheableEntry = info.isCacheable;
								}

								if (isCacheableEntry)
								{
									string cacheKey = entry.versionString;
									do
									{
										// Going to cache the same value and same cachePath because it has already been constructed
										// with the lower level versioning path in the hierarchy; this is for the keys
										// for the lower level versioning strings
										cacheEntry(cachePath, cacheKey, val);
									} while ((cacheKey = ((pendingCacheKeys.Count > 0) ? pendingCacheKeys.Pop() as string : null)) != null);
								}
							}
							else
							{
								//This entry was already cached under a shorter versioned string
								// re-cache it under the new full string
								cacheEntry(cachePath, name, val);
							}
							break;
						}
						else
						{
							pendingCacheKeys.Push(entry.versionString);
						}
						idx++;
					}
				}
			}

			if (val != null)
			{
                string remainingName = nameInfo.RemainingName == "" ? null : nameInfo.RemainingName;
				if (val is INameAddressSupport)
					if (infoArgs != null && infoArgs.Length > 0)
                        val = ((INameAddressSupport)val).get(remainingName, infoArgs);
					else
                        val = ((INameAddressSupport)val).get(remainingName);
			}
			else
			{
				throw new CatalogException(String.Format("Unable to create an instance of '{0}' from catalog", name));
			}
			return val;
		}
		private object getFromXmlFile(IApplication app, NameInfo name, FileInfo f, string cachePath, params object[] infoArgs)
		{
			object val;
			XmlDocument xmlDoc;
			try
			{
				xmlDoc = new XmlDocument();
				xmlDoc.Load("filecrypto://" + f.FullName.Replace(@"\\", @"").Replace(@"\", @"/"));
			}
			catch (Exception e)
			{
				throw new CatalogException(
					String.Format("Cannot load XML for catalog entry '{0}'", name.CatalogEntryName), e);
			}

			string factoryName = "GeneralContainerNodes";
			string className = m_className;
			if (m_usingSpecifiedClassFactory)
			{
				factoryName = m_className;
				className = xmlDoc.DocumentElement.Name;
			}
			object[] args = null;
			if (infoArgs != null && infoArgs.Length > 0)
			{
				args = new object[infoArgs.Length + 1];
				args[0] = xmlDoc.DocumentElement;
				for (int pos = 0; pos < infoArgs.Length; ++pos)
					args[pos + 1] = infoArgs[pos];
			}
			else
				args = new object[] { xmlDoc.DocumentElement };

			if (className != null  && className.Length > 0)
			{
				IClassFactory factory;
				factory = app.factories[factoryName];
				if (factory == null)
					throw new CatalogException(
						String.Format("Cannot locat factory named '{0}' for entry '{1}'", factoryName, name.CatalogEntryName));

				try
				{
					if ((val = factory.createFromPattern(className, xmlDoc.DocumentElement, name.CatalogEntryName)) == null &&
						(!m_usingSpecifiedClassFactory))
					{
						val = Cmn.callCtor(m_className,
							new Type[] { xmlDoc.DocumentElement.GetType(), typeof(String) }, new object[] { xmlDoc.DocumentElement, name.CatalogEntryName });
					}
				}
				catch (Exception e)
				{
					try
					{
						val = factory.createFromPattern(className, xmlDoc.DocumentElement, name);
					}
					catch (Exception e2)
					{
						throw new CatalogException(String.Format(
							"Unable to create an instance of '{0}' from the factory '{1}'", name.CatalogEntryName, factoryName), e2);
					}
				}
			}
			else
			{
				val = xmlDoc.DocumentElement;
			}

			// Removed cache entry here let the get method handle it
//			ICatalogItem info = val as ICatalogItem;
//			if (info == null || info.isCacheable)
//			{
//				cacheEntry(cachePath, name.CatalogCacheKey, val);
//				//m_map.put(name, val, m_childType);
//			}
			
			return val;
		}
		protected FileInfo getCatalogFileInfo(string path, string name, ref string cachePaths)
		{
			string[] paths;
			if (path.IndexOf(',') > -1)
				paths = path.Split(',');
			else
				paths = new string[] { path };
			FileInfo f = null;
			foreach (string thisPath in paths)
			{
				f = getFileInfo(thisPath, name);
				if (cachePaths.Length == 0)
				{
					cachePaths = f.FullName;
				}
				else
				{
					cachePaths += "," + f.FullName;
				}
				if (f.Exists)
				{
					break;
				}
			}
			return f;
		}
		private FileInfo getFileInfo(string path, string name)
		{
			FileInfo f = new FileInfo(Path.Combine(path, name + ".enc"));
			if (!f.Exists)
				f = new FileInfo(Path.Combine(path, name + ".xml"));
			return f;
		}


		/// <summary>
		/// Overridden method to determine whether a requested key exists in this catalog.  This method
		/// goes directly to the file system to search for the requested key.  If the exact key is not found,
		/// it uses the versioning algorithm to see if a versioned match is found.
		/// </summary>
		/// <param name="nameAddress"></param>
		/// <returns></returns>
		public override bool containsField(string nameAddress)
		{

			if (m_type == FileType.Directory)
			{
				string newName = nameAddress;

				//if the versioning partner is configured, use it to resolve the name.
				if (m_versioning != null)
				{
					newName = m_versioning.resolve(newName);
				}

				while ((!newName.Equals("")) && (!File.Exists(Path.Combine(m_base.FullName, newName.Replace("::", ".") + ".xml"))))
				{
					//Remove the last delimited value from newKey and continue
					int delimiterLocation = newName.LastIndexOf(m_versioningDelimiter);
					if (delimiterLocation > 0)
					{
						newName = newName.Substring(0, delimiterLocation);
					}
					else
					{
						//There are no more delimited values, set new key to "" so we can exit from the loop.
						newName = "";
					}
				}
				if (!newName.Equals(""))
					return File.Exists(Path.Combine(m_base.FullName, newName.Replace("::", ".") + ".xml"));
			}
			return false;
		}

		#endregion

	}
}
